{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<div align=\"right\"><i>Peter Norvig<br>2012<br>updated 2019, 2023</i></div>\n",
    "\n",
    "# Weighing Twelve Balls on a Scale: ①②③④ ⚖ ⑤⑥⑦⑧ \n",
    "\n",
    "Here is [a classic](https://en.wikipedia.org/wiki/Balance_puzzle) brain-teaser puzzle from 1945:  \n",
    "\n",
    "- *You are given twelve identical-looking balls and a two-sided balance scale. One of the balls is of a different weight, although you don't know whether it is lighter or heavier. How can you use just three weighings of the scale to determine not only what the different ball is, but also whether it is lighter or heavier?*\n",
    "\n",
    "\n",
    "I would like to solve not just this specific puzzle, but related puzzles that vary:\n",
    "- The number of balls.\n",
    "- The number of weighings.\n",
    "- The possibilities for the odd ball: maybe we know that it some ball is lighter, and can not be heavier. Or maybe one possibility is that all the balls actually weigh the same.\n",
    "- (However, it will never be the case that *two* or more balls are different from the rest. That would change the puzzle too much.)\n",
    "\n",
    "If I'm going to solve a puzzle with dozens or hundreds of balls, I'd rather use a computer program, not pencil and paper (as was intended for the 1945 version of the puzzle). I originally wrote such a program in Lisp around 1980, ported it to Python in 2012, and decided to publish it here as a notebook after seeing continued interest in the puzzle:\n",
    "- A [Numberplay column](https://wordplay.blogs.nytimes.com/2014/07/21/12coin/) in 2014.\n",
    "- A [MathWorld article](https://mathworld.wolfram.com/Weighing.html) in 2017.\n",
    "- A [Wikipedia article](https://en.wikipedia.org/wiki/Balance_puzzle) that was started in 2018.\n",
    "- A [538 Riddler column](https://fivethirtyeight.com/features/which-billiard-ball-is-rigged/)  2019.\n",
    "- A [Mind-Benders for the Quarantined! entry](https://momath.org/civicrm/?page=CiviCRM&q=civicrm/event/info&reset=1&id=1620) in 2020. \n",
    "\n",
    "\n",
    "# Defining the Vocabulary\n",
    "    \n",
    "Here are the main concepts, and how I implement them in Python:\n",
    "\n",
    "- **Puzzle**: A dataclass holding a description of the number of balls, number of allowable weighings, and the possible oddballs.\n",
    "- **Ball**: A positive integer from 1 to some *N*. \n",
    "- **Oddball**: The one ball that is different from the others, and the way it is different: heavier or lighter. I'll use, e.g., `+3` to mean that ball `3` is heavier than the rest, `-3` to mean that ball `3` is lighter than the rest, and `0` ito mean that all the balls actually weigh the same.\n",
    "- **Weighing**:  a dataclass describing the set of balls on each side of the scale, and the three possible outcomes of the weighing.\n",
    "- **Outcome**: The outcome of a weighing is that the weight of the left side is greater than, equal to, or less than the right side.\n",
    "- **Partition**: Each weighing  *partitions* the set of possible oddballs into three subsets, corresponding to the three possible outcomes.\n",
    "- **Rule of 3**: A key insight is that two weighings partition the set of possible oddballs into 3 x 3 = 9 subsets, three weighings into 3 x 3 x 3 = 27 subsets, and <i>w</i> weighings into  3<sup><i>w</i></sup> subsets.  \n",
    "- **Good/Bad Partitions**: A weighing that leaves more  3<sup>(<i>w</i>-1)</sup> oddballs in any outcome branch is said to be a **bad partition**, and could never lead to a solution. A weighing that does not do that is a **good partition** (but might not lead to a solution).`\n",
    "- **Policy tree**: A tree where the interior nodes are weighings and rthe leaves are sets of oddballs. \n",
    "- **Solution**: A solution to a puzzle is a policy tree in which no path has more than the allowable number of weighings; every possible path ends with the identification of a single oddball; and all the oddballs are covered.\n",
    "\n",
    "# Implementation of Basic Data Types and Constants\n",
    "\n",
    "Some imports and definitions of data types and constants:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "from dataclasses import dataclass\n",
    "from typing import *\n",
    "import random\n",
    "random.seed(42) # For reproducability\n",
    "\n",
    "empty      = frozenset()           # The empty set\n",
    "lt, eq, gt = 'lt', 'eq', 'gt'      # The 3 possible outcomes of a weighing (`lt` means the left weighs less than the right)\n",
    "Outcome    = Literal[lt, eq, gt]   # Possible outcomes of a weighing\n",
    "Ball       = int                   # Balls are positive integers\n",
    "Oddball    = int                   # An Oddball is an integer: positive (heavier), negative (lighter), or zero (same weight)\n",
    "Tree       = Union['Weighing', Set[Oddball]] # A policy tree has Weighings for interior nodes and oddball sets for leaf nodes"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# The `Puzzle` class\n",
    "\n",
    "`Puzzle` is a data class that records the number of balls, the number of allowable weighings, a collection of ball numbers, and the possible oddballs. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 94,
   "metadata": {},
   "outputs": [],
   "source": [
    "@dataclass\n",
    "class Puzzle:\n",
    "    \"\"\"A ball-weighing puzzle.\"\"\"\n",
    "    N:         int              # the number of balls that can be weighed\n",
    "    weighings: int              # the number of weighins\n",
    "    balls:     Collection[Ball] # all the balls in the puzzle\n",
    "    oddballs:  Set[Oddball]     # the allowable oddball numbers"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "The convenience function `make_puzzle` allows you to specify the oddballs by setting the parameter `odd` to be either a set of oddball integers, or a string of characters:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 95,
   "metadata": {},
   "outputs": [],
   "source": [
    "def make_puzzle(N, weighings, odd='+-'):\n",
    "    \"\"\"Create a puzzle with `N` balls, and the given number of `weighings`.\n",
    "    `odd` is a collection of numbers, or a string with some of the characters `+-=`,\n",
    "    meaning any balls might be heavy (`+`), light (`-`), or equal (`=`).\"\"\"\n",
    "    balls = range(1, N + 1)\n",
    "    if isinstance(odd, str):\n",
    "        sign = {'+': +1, '-': -1, '=': 0}\n",
    "        oddballs = {sign[ch] * b for b in balls for ch in odd}\n",
    "    else:\n",
    "        oddballs = set(odd)\n",
    "    return Puzzle(N, weighings, balls, oddballs)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# The `Weighing` class\n",
    "\n",
    "`Weighing` is a simple data class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "@dataclass\n",
    "class Weighing:\n",
    "    \"\"\"A weighing of the balls on the left (`L`) against the balls on the right (`R`),\n",
    "    with three branches `gt`, `eq`, and `lt` for the three outcomes of the weighing.\"\"\"\n",
    "    L:  List[Ball]\n",
    "    R:  List[Ball]\n",
    "    gt: Tree\n",
    "    eq: Tree\n",
    "    lt: Tree"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The function call `weigh(L, R, oddballs)` weighs the  `L` balls against the `R` balls assuming that only the given `oddballs` are possible. The three outcome branches partition the set of `oddballs`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "metadata": {},
   "outputs": [],
   "source": [
    "def weigh(L: Collection[Ball], R: Collection[Ball], oddballs: Set[Oddball]) -> Weighing:\n",
    "    assert len(L) == len(R)\n",
    "    weighing = Weighing(L, R, set(), set(), set())\n",
    "    # Partition the oddballs into the three outcome branches\n",
    "    for b in oddballs:\n",
    "        if   +b in L or -b in R: weighing.gt.add(b)\n",
    "        elif +b in R or -b in L: weighing.lt.add(b)\n",
    "        else:                    weighing.eq.add(b)\n",
    "    return weighing"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here's an example weighing where there are 5 balls, and the oddball can be lighter, heavier, or there might be no oddball:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Weighing(L=[1, 2], R=[4, 5], gt={1, 2, -5, -4}, eq={0, 3, -3}, lt={4, 5, -2, -1})"
      ]
     },
     "execution_count": 49,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "weigh([1, 2], [4, 5], range(-5, 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The notation `gt={1, 2, -5, -4}` means that if the outcome is that the left balance pan is heavier than the right then the remaining possibilities are that the 1 or 2 ball might be heavy, or the 4 or 5 ball might be light. \n",
    "\n",
    "# `Tree` Functions\n",
    "\n",
    "Here are some straightforward utility functions on trees:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "def is_solution(tree, puzzle) -> bool:\n",
    "    \"\"\"Is this policy tree a solution to the puzzle?\n",
    "    It is if it all the leaves are singletons or empty, and every oddball appears.\"\"\"\n",
    "    return (all(len(leaf) <= 1 for leaf in leaves(tree)) \n",
    "            and set().union(*leaves(tree)) == puzzle.oddballs)\n",
    "\n",
    "def leaves(tree: Weighing) -> List[Set[Oddball]]:\n",
    "    \"\"\"A list of all leaves of the tree.\"\"\"\n",
    "    return (leaves(tree.gt) + leaves(tree.eq) + leaves(tree.lt)\n",
    "            if isinstance(tree, Weighing)\n",
    "            else [tree])\n",
    "    \n",
    "def branches(tree: Weighing) -> tuple:\n",
    "    \"\"\"The three branches of a weighing.\"\"\"\n",
    "    return (tree.gt, tree.eq, tree.lt)\n",
    "\n",
    "def is_good_partition(weighing: Weighing, w: int) -> bool:\n",
    "    \"Does this weighing partition the oddballs in a way that might be solved in `w` more weighings?\"\n",
    "    return all(len(oddballs) <= 3 ** w for oddballs in branches(weighing))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's write some simple tests for these functions:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 125,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 125,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def test1() -> bool:\n",
    "    puzzle3 = make_puzzle(3, 1, odd='+')\n",
    "    solution3 = weigh([1], [3], {1, 2, 3})\n",
    "    \n",
    "    assert solution3 == Weighing(L=[1], R=[3], gt={1}, eq={2}, lt={3})\n",
    "    assert is_solution(solution3, puzzle3)\n",
    "    assert leaves(solution3) == [{1}, {2}, {3}]\n",
    "    assert branches(solution3) == ({1}, {2}, {3})\n",
    "\n",
    "    assert make_puzzle(3, 1, '+').oddballs   == {1, 2, 3}\n",
    "    assert make_puzzle(3, 1, '-').oddballs   == {-1, -2, -3}\n",
    "    assert make_puzzle(3, 1, '+-').oddballs  == {1, 2, 3, -1, -2, -3}\n",
    "    assert make_puzzle(3, 1, '+=').oddballs  == {0, 1, 2, 3}\n",
    "    assert make_puzzle(3, 1, '+-=').oddballs == {0, 1, 2, 3, -1, -2, -3}\n",
    "    assert make_puzzle(3, 1, '+-=') == Puzzle(N=3, weighings=1, balls=range(1, 4), oddballs={0, 1, 2, 3, -2, -3, -1})\n",
    "\n",
    "    assert not is_good_partition(weigh([1, 2, 3],       [10, 11, 12], puzzle12.oddballs), 2)\n",
    "    assert     is_good_partition(weigh([1, 2, 3, 4], [9, 10, 11, 12], puzzle12.oddballs), 2)\n",
    "    \n",
    "    return True\n",
    "\n",
    "test1()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Good and Bad Partitions\n",
    "\n",
    "Consider a puzzle with 12 balls and 3 weighings. Assume we start by weighing balls [7, 8, 9] against [10, 11, 12]: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 97,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Weighing(L=[7, 8, 9], R=[10, 11, 12], gt={7, 8, 9, -12, -11, -10}, eq={1, 2, 3, 4, 5, 6, -2, -6, -5, -4, -3, -1}, lt={10, 11, 12, -9, -8, -7})"
      ]
     },
     "execution_count": 97,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "puzzle12 = make_puzzle(N=12, weighings=3)\n",
    "\n",
    "weigh([7, 8, 9], [10, 11, 12], puzzle12.oddballs)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If this was the first weighing in our policy tree, could we go on to solve the puzzle? \n",
    "\n",
    "The answer is: **No!** If the outcome is `eq` (the scale balances) then there are 12 possible oddballs remaining (any of 1–6 could be either heavy or light). We only have *w* = 2 weighings left, and 12 > 3<sup>2</sup>, so the **rule of 3** tells us it is impossible to reach a solution from here.  This a **bad partition**. \n",
    "\n",
    "Here's a **good partition**, in which no branch has more than 3<sup><i>2</i></sup> = 9 oddballs:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Weighing(L=[5, 6, 7, 8], R=[9, 10, 11, 12], gt={5, 6, 7, 8, -12, -11, -10, -9}, eq={1, 2, 3, 4, -2, -4, -3, -1}, lt={9, 10, 11, 12, -8, -7, -6, -5})"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "weigh([5, 6, 7, 8], [9, 10, 11, 12], puzzle12.oddballs)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Solving Puzzles\n",
    "\n",
    "So now we have a viable approach to implementing `solve(puzzle)`: build a policy tree that partitions oddballs down to singletons (or give up and return `None` if that can't be done in the allowable number of weighings). For each subset of oddballs in a partition, recursively call `solve` on a new subpuzzle. Since we don't know for sure which balls to weigh at each point, call `good_partitions` to generate multiple possible good partitions, and try to find one that works."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 71,
   "metadata": {},
   "outputs": [],
   "source": [
    "def solve(puzzle) -> Optional[Tree]:\n",
    "    \"\"\"Find a tree that covers all the oddballs in the puzzle within the given number of weighings.\"\"\"\n",
    "    N, w, oddballs = puzzle.N, puzzle.weighings, puzzle.oddballs\n",
    "    if len(oddballs) <= 1:         # No choices left; this branch is complete\n",
    "        return oddballs\n",
    "    elif len(oddballs) > 3 ** w:   # Impossible to solve from here\n",
    "        return None\n",
    "    elif w > 0:                    # Find a partition that works\n",
    "        for partition in good_partitions(puzzle):\n",
    "            partition.lt = solve(make_puzzle(N, w - 1, partition.lt))\n",
    "            partition.eq = solve(make_puzzle(N, w - 1, partition.eq))\n",
    "            partition.gt = solve(make_puzzle(N, w - 1, partition.gt))\n",
    "            if None not in branches(partition):\n",
    "                return partition\n",
    "    return None            # No solution; failure"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Much of the work is done by `good_partitions`, a generator that yields good partitions. For now, I'm not going to try to be clever about choosing partitions. For values of `B` from 1 to `N/2`, I'll try to weigh the first `B`  balls on the left against the last `B` balls on the right. If that results ina good partition, I'll yield it. Then I'll randomly shuffle the balls and repeat the whole process (`repeat` times), so that I'll get different sets of balls on the left and right on each try."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 101,
   "metadata": {},
   "outputs": [],
   "source": [
    "def good_partitions(puzzle, repeat=100) -> Iterable[Weighing]:\n",
    "    \"Yield random good partitions.\"\n",
    "    oddballs, w, balls = puzzle.oddballs, puzzle.weighings, sorted(puzzle.balls)\n",
    "    for _ in range(repeat): \n",
    "        for B in range(1, len(balls) // 2 + 1):\n",
    "            L, R = balls[:B], balls[-B:]\n",
    "            partition = weigh(L, R, oddballs)\n",
    "            if is_good_partition(partition, w - 1):\n",
    "                yield partition\n",
    "        random.shuffle(balls)  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can check to see if `solve` does the job:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 103,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Weighing(L=[1, 2, 3, 4], R=[9, 10, 11, 12], gt=Weighing(L=[1, 8, 3, 7, 10], R=[2, 9, 5, 4, 6], gt=Weighing(L=[7, 4, 3], R=[11, 5, 1], gt={3}, eq={-9}, lt={1}), eq=Weighing(L=[1], R=[12], gt={-12}, eq={-11}, lt=set()), lt=Weighing(L=[4, 8, 9], R=[2, 11, 7], gt={4}, eq={-10}, lt={2})), eq=Weighing(L=[2, 1, 10, 9, 11], R=[6, 12, 5, 3, 8], gt=Weighing(L=[1, 2, 3, 4, 5], R=[8, 9, 10, 11, 12], gt={-8}, eq={-6}, lt={-5}), eq=Weighing(L=[1, 2, 3, 4, 5, 6], R=[7, 8, 9, 10, 11, 12], gt={-7}, eq=set(), lt={7}), lt=Weighing(L=[1, 2, 3, 4, 5], R=[8, 9, 10, 11, 12], gt={5}, eq={6}, lt={8})), lt=Weighing(L=[11, 4, 2, 10], R=[3, 12, 7, 6], gt=Weighing(L=[10, 5, 6, 12], R=[11, 8, 1, 9], gt={10}, eq={-3}, lt={11}), eq=Weighing(L=[1], R=[12], gt=set(), eq={9}, lt={-1}), lt=Weighing(L=[7, 11, 8, 10, 1], R=[12, 6, 4, 5, 3], gt={-4}, eq={-2}, lt={12})))"
      ]
     },
     "execution_count": 103,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "solve(puzzle12)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The good news is that it appears to get an answer; the bad news is that the output is really hard to read. \n",
    "\n",
    "Let's look at a trivial puzzle: 3 balls, 1 weighing allowed, and the oddball can only be heavier:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 104,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Weighing(L=[1], R=[3], gt={1}, eq={2}, lt={3})"
      ]
     },
     "execution_count": 104,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "solve(make_puzzle(3, 1, odd='+'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This trivial policy tree says you weigh the first ball against the third (leaving the second unweighed), and the three possible weighing outcomes tell you which of the three balls  is heavier. That looks right."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Prettier Output\n",
    "\n",
    "Let's make the output easier to read. The function`do(puzzle)` will solve the puzzle, verify that the tree is valid, and print the tree in a pretty format using the function `pretty_tree(tree)`. The format is:\n",
    "- Balls are represented by Unicode characters: `①` or `②`.\n",
    "- An oddball is represented with a plus or minus sign for heavy or light: `+①` or `-②`.\n",
    "- A weighing is represented by a string with a balance scale symbol: `[①② ⚖ ③④] ➔`.\n",
    "- The three branches of a tree are represented by the prefixes \"`>:`\" or \"`=:`\" or \"`<:`\".\n",
    "- The empty set of oddballs (meaning no ball is heavier) is represented by `∅`.\n",
    "- An impossible branch is represented by the character `!` (as when weighing have ① > ② > ③).\n",
    "- Branches are on indented lines, unless the branches are all leaves."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 132,
   "metadata": {},
   "outputs": [],
   "source": [
    "def do(puzzle) -> None:\n",
    "    \"Solve the puzzle; verify the solution; and print the solution in a pretty format.\"\n",
    "    tree = solve(puzzle)\n",
    "    assert (tree is None) or is_solution(tree, puzzle)\n",
    "    print(pretty_tree(tree))\n",
    "    \n",
    "def pretty_tree(tree, depth=0, prefix='') -> str:\n",
    "    \"Pretty, indented string representing a policy tree.\"\n",
    "    if tree is None:\n",
    "        return 'No solution'\n",
    "    elif isinstance(tree, Weighing):\n",
    "        subtrees = (pretty_tree(tree.gt, depth + 1, '>:'), \n",
    "                    pretty_tree(tree.eq, depth + 1, '=:'), \n",
    "                    pretty_tree(tree.lt, depth + 1, '<:'))\n",
    "        indent   = ('' if depth == 0 else ('\\n' + '   ' * depth))\n",
    "        return f'{indent}{prefix}[{ball_str(tree.L)} ⚖ {ball_str(tree.R)}] ➔ {\" \".join(subtrees)}'\n",
    "    else: # tree is a set of oddballs\n",
    "        return f'{prefix}{oddball_str(tree)}'\n",
    "    \n",
    "def ball_str(balls: List[Ball]) -> str:\n",
    "    \"\"\"Unicode character string representing a list of balls.\"\"\"\n",
    "    return cat(BALLS[i] if (0 <= i < len(BALLS)) else f'({i})' \n",
    "               for i in sorted(balls))\n",
    "\n",
    "def oddball_str(oddballs) -> str:\n",
    "    \"\"\"Unicode character string representing a list of oddballs.\n",
    "    Normally a string like '-①+②', but the empty list returns '!'.\"\"\"\n",
    "    if not oddballs:\n",
    "        return '!'\n",
    "    else:\n",
    "        return cat((\"+\" if b > 0 else \"-\" if b < 0 else \"\") + ball_str([abs(b)]) \n",
    "                   for b in oddballs)\n",
    "\n",
    "cat = ''.join\n",
    "\n",
    "BALLS = ('∅①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳㉑㉒㉓㉔㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟㊱㊲㊳㊴㊵㊶㊷㊸㊹㊺㊻㊼㊽㊾㊿'\n",
    "         'ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏ'\n",
    "         'ⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ'\n",
    "         '⦰⦱⦲⦳⦴⦵⦶⦷⦸⦹⦺⦻⦼⦽⦾⦿⧀⧁⧂⧃⨀⨁⨂⨴⨵⨶⨷⨸⓵⓶⓷⓸⓹⓺⓻⓼⓽⓾')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Easy Puzzle Solutions\n",
    "\n",
    "Now that the results will be readable, let's solve some puzzles:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 133,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[① ⚖ ③] ➔ >:+① =:+② <:+③\n"
     ]
    }
   ],
   "source": [
    "# The trivial puzzle from before: 3 balls, 1 weighing, only heavier balls possible\n",
    "do(make_puzzle(3, 1, odd='+'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 134,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[①②③ ⚖ ⑦⑧⑨] ➔ \n",
      "   >:[②⑤ ⚖ ③④] ➔ >:+② =:+① <:+③ \n",
      "   =:[①②③④ ⚖ ⑥⑦⑧⑨] ➔ >:+④ =:+⑤ <:+⑥ \n",
      "   <:[①④⑤⑧ ⚖ ②③⑥⑨] ➔ >:+⑧ =:+⑦ <:+⑨\n"
     ]
    }
   ],
   "source": [
    "# 9 balls, 2 weighings, odd ball can only be heavier\n",
    "do(make_puzzle(9, 2, '+'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 130,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(① ⚖ ③) ➔ \n",
      "   >:(② ⚖ ①) ➔ >:! =:-③ <:+① \n",
      "   =:(② ⚖ ③) ➔ >:+② =:∅ <:-② \n",
      "   <:(① ⚖ ②) ➔ >:! =:+③ <:-①\n"
     ]
    }
   ],
   "source": [
    "# 3 balls, 2 weighings, lighter, heavier or equal balls possible\n",
    "do(make_puzzle(3, 2, '+-='))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Six of the outcomes (of the second weighings) correctly identify the numbered oddball. But there are three special outcomes:\n",
    "- `=:∅` means there is no oddball; all balls weigh the same.\n",
    "- `>:!` means this outcome is impossible (because it implies that `② > ① > ③`, and we know only one ball can have an odd weight).\n",
    "- `<:!` also indicates an impossible outcome (because it implies `① < ③ < ②`).\n",
    "\n",
    "Let's look at the original puzzle with 12 balls and 3 weighings. We'll run it twice and get two random solutions:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 135,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[①②③④ ⚖ ⑨⑩⑪⑫] ➔ \n",
      "   >:[①②⑦⑫ ⚖ ③④⑥⑩] ➔ \n",
      "      >:[②⑥⑦⑪⑫ ⚖ ①③⑤⑧⑨] ➔ >:+② =:-⑩ <:+① \n",
      "      =:[①② ⚖ ⑪⑫] ➔ >:-⑪ =:-⑨ <:! \n",
      "      <:[④⑧⑩⑪⑫ ⚖ ①②⑤⑥⑦] ➔ >:+④ =:+③ <:-⑫ \n",
      "   =:[④⑧⑩⑫ ⚖ ②⑥⑦⑨] ➔ \n",
      "      >:[⑥⑧⑨⑩ ⚖ ①②③④] ➔ >:+⑧ =:-⑦ <:-⑥ \n",
      "      =:[①②③④⑤ ⚖ ⑧⑨⑩⑪⑫] ➔ >:+⑤ =:! <:-⑤ \n",
      "      <:[⑥⑧⑪ ⚖ ①②④] ➔ >:+⑥ =:+⑦ <:-⑧ \n",
      "   <:[①③⑦⑩ ⚖ ④⑤⑧⑨] ➔ \n",
      "      >:[①②③ ⚖ ⑩⑪⑫] ➔ >:! =:-④ <:+⑩ \n",
      "      =:[⑫ ⚖ ⑪] ➔ >:+⑫ =:-② <:+⑪ \n",
      "      <:[⑤⑦⑧ ⚖ ③⑨⑩] ➔ >:-③ =:-① <:+⑨\n"
     ]
    }
   ],
   "source": [
    "do(puzzle12)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 136,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[①②③④ ⚖ ⑨⑩⑪⑫] ➔ \n",
      "   >:[③⑦⑫ ⚖ ②⑨⑩] ➔ \n",
      "      >:[②⑩ ⚖ ⑦⑨] ➔ >:-⑨ =:+③ <:-⑩ \n",
      "      =:[①②⑤ ⚖ ④⑥⑩] ➔ >:+① =:-⑪ <:+④ \n",
      "      <:[① ⚖ ⑫] ➔ >:-⑫ =:+② <:! \n",
      "   =:[②③④⑦⑧ ⚖ ①⑥⑨⑪⑫] ➔ \n",
      "      >:[②⑤⑦⑨⑩ ⚖ ③④⑧⑪⑫] ➔ >:+⑦ =:-⑥ <:+⑧ \n",
      "      =:[①②③④⑤ ⚖ ⑧⑨⑩⑪⑫] ➔ >:+⑤ =:! <:-⑤ \n",
      "      <:[①③⑥⑦⑨ ⚖ ②④⑤⑩⑪] ➔ >:+⑥ =:-⑧ <:-⑦ \n",
      "   <:[④⑤⑨ ⚖ ①②⑪] ➔ \n",
      "      >:[②⑪ ⚖ ①⑤] ➔ >:-① =:+⑨ <:-② \n",
      "      =:[③⑤⑥⑧⑩ ⚖ ②④⑦⑨⑪] ➔ >:+⑩ =:+⑫ <:-③ \n",
      "      <:[①② ⚖ ⑪⑫] ➔ >:! =:-④ <:+⑪\n"
     ]
    }
   ],
   "source": [
    "do(puzzle12)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "tags": []
   },
   "source": [
    "I note that the traditional answer to the 12-ball puzzle weighs 4 balls on each side of the first weighing, three balls on the second weighing, and 1 ball on each side on the third weighing. But my program  won't necessarily minimize the number of balls on each side of the scale, nor make one branch of the tree be symmetric with another branch.\n",
    "\n",
    "# Other Puzzles with 3 Weighings\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can do **12 balls in 3 weighings** even when we add in the possibility that there is no oddball–that all the balls weigh the same:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "①②③④ ⚖ ⑨⑩⑪⑫ ➔ \n",
      "   >:④⑧⑫ ⚖ ③⑩⑪ ➔ \n",
      "      >:④⑤⑥⑩ ⚖ ①③⑦⑧ ➔ >:+④ =:-⑪ <:-⑩ \n",
      "      =:②④ ⚖ ①⑩ ➔ >:+② =:-⑨ <:+① \n",
      "      <:① ⚖ ⑫ ➔ >:-⑫ =:+③ <:! \n",
      "   =:①②③⑨ ⚖ ⑥⑦⑧⑪ ➔ \n",
      "      >:③⑦ ⚖ ⑧⑪ ➔ >:-⑧ =:-⑥ <:-⑦ \n",
      "      =:①②③④⑤ ⚖ ⑧⑨⑩⑪⑫ ➔ >:+⑤ =:∅ <:-⑤ \n",
      "      <:①⑦ ⚖ ②⑥ ➔ >:+⑦ =:+⑧ <:+⑥ \n",
      "   <:②⑤⑦⑧ ⚖ ①③④⑩ ➔ \n",
      "      >:①②⑧⑩⑫ ⚖ ③⑤⑦⑨⑪ ➔ >:-③ =:-④ <:-① \n",
      "      =:②⑪ ⚖ ⑧⑫ ➔ >:+⑪ =:+⑨ <:+⑫ \n",
      "      <:①② ⚖ ⑪⑫ ➔ >:! =:+⑩ <:-②\n"
     ]
    }
   ],
   "source": [
    "do(make_puzzle(12, 3, odd='+-='))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Can we solve the **13-light-or-heavy-balls in 3 weighings** puzzle, which has 26 possibilities? After all, 26 is less than 3<sup>3</sup> = 27."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "No solution\n"
     ]
    }
   ],
   "source": [
    "puzzle13 = make_puzzle(13, 3, odd='+-')\n",
    "do(puzzle13)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**No**, and we can see why by looking at the sizes of the three partitions. For the first weighing we need to find some number of balls *B* to put on each side of the scale so that there will be no more than 9 balls in any partition. It seems best to choose a *B* that is near *N*/3, where *N* is the number of balls in the puzzle, to partition the oddballs into thirds. \n",
    "\n",
    "So for *N* = 13, it looks like *B* = 4 or 5 balls is the best bet, but to help figure it out, I'll define the function `partition_sizes` to build a dict that, for various numbers *B* of balls gives the sizes of the three resulting branches of the partition, and tells whether the partition is *good* or *bad*."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [],
   "source": [
    "def partition_sizes(puzzle) -> Dict[int, Tuple[int, int, int]]:\n",
    "    \"\"\"For various nunbers of balls B to weigh, what are the sizes of the three branches,\n",
    "    and is the partition good?\"\"\"\n",
    "    third = puzzle.N // 3\n",
    "    result = {}\n",
    "    for B in range(third - 2, third + 4):\n",
    "        L, R = puzzle.balls[:B], puzzle.balls[-B:]\n",
    "        partition = weigh(L, R, puzzle.oddballs)\n",
    "        lengths = map(len, branches(partition))\n",
    "        good = 'Good' if is_good_partition(partition, puzzle.weighings - 1) else 'Bad'\n",
    "        result[B] = (good, *lengths)\n",
    "    return result   "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{2: ('Bad', 4, 18, 4),\n",
       " 3: ('Bad', 6, 14, 6),\n",
       " 4: ('Bad', 8, 10, 8),\n",
       " 5: ('Bad', 10, 6, 10),\n",
       " 6: ('Bad', 12, 2, 12),\n",
       " 7: ('Bad', 14, 0, 12)}"
      ]
     },
     "execution_count": 27,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "partition_sizes(puzzle13)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We see that whatever value we choose for `B`, we will always get a bad partition of the 13-ball puzzle (there will always be at least one branch with more than 9 oddballs). \n",
    "\n",
    "For the puzzle with 12 balls (and thus 24 oddballs), the only good partition is with 4 balls on each side of the balance:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{2: ('Bad', 4, 16, 4),\n",
       " 3: ('Bad', 6, 12, 6),\n",
       " 4: ('Good', 8, 8, 8),\n",
       " 5: ('Bad', 10, 4, 10),\n",
       " 6: ('Bad', 12, 0, 12),\n",
       " 7: ('Bad', 14, 0, 10)}"
      ]
     },
     "execution_count": 28,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "partition_sizes(puzzle12)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can do **27 balls in 3 weighings** if we know that the oddball can only be heavier, not lighter:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{7: ('Bad', 7, 13, 7),\n",
       " 8: ('Bad', 8, 11, 8),\n",
       " 9: ('Good', 9, 9, 9),\n",
       " 10: ('Bad', 10, 7, 10),\n",
       " 11: ('Bad', 11, 5, 11),\n",
       " 12: ('Bad', 12, 3, 12)}"
      ]
     },
     "execution_count": 29,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "puzzle27 = make_puzzle(27, 3, '+')\n",
    "partition_sizes(puzzle27)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "①②③④⑤⑥⑦⑧⑨ ⚖ ⑲⑳㉑㉒㉓㉔㉕㉖㉗ ➔ \n",
      "   >:④⑥⑧⑬⑭⑱⑲㉒㉕ ⚖ ①②⑨⑩⑮⑰⑳㉓㉔ ➔ \n",
      "      >:④⑳ ⚖ ⑧⑫ ➔ >:+④ =:+⑥ <:+⑧ \n",
      "      =:④⑤⑬⑭⑱㉖ ⚖ ⑦⑨⑩⑪⑯⑲ ➔ >:+⑤ =:+③ <:+⑦ \n",
      "      <:④⑨⑭⑮⑯⑱⑳㉔㉕㉗ ⚖ ①③⑤⑥⑦⑧⑩⑬⑲㉓ ➔ >:+⑨ =:+② <:+① \n",
      "   =:①②③④⑤⑥⑦⑧⑨⑩⑪⑫ ⚖ ⑯⑰⑱⑲⑳㉑㉒㉓㉔㉕㉖㉗ ➔ \n",
      "      >:④⑤⑦⑩⑭⑮⑳ ⚖ ①⑧⑨⑫⑰㉒㉖ ➔ >:+⑩ =:+⑪ <:+⑫ \n",
      "      =:①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬ ⚖ ⑮⑯⑰⑱⑲⑳㉑㉒㉓㉔㉕㉖㉗ ➔ >:+⑬ =:+⑭ <:+⑮ \n",
      "      <:②④⑩⑪⑭⑮⑯⑲㉒㉔㉕ ⚖ ③⑤⑦⑧⑬⑰⑳㉑㉓㉖㉗ ➔ >:+⑯ =:+⑱ <:+⑰ \n",
      "   <:②⑦⑨⑮⑯㉒㉓㉔ ⚖ ③⑥⑩⑬⑱⑳㉑㉕ ➔ \n",
      "      >:①⑤⑥⑪⑱⑲㉑㉔㉖ ⚖ ④⑦⑨⑩⑫⑭⑰⑳㉒ ➔ >:+㉔ =:+㉓ <:+㉒ \n",
      "      =:⑤⑯㉓㉗ ⚖ ①⑪⑱㉖ ➔ >:+㉗ =:+⑲ <:+㉖ \n",
      "      <:①③④⑥㉑ ⚖ ⑨⑫⑬㉔㉕ ➔ >:+㉑ =:+⑳ <:+㉕\n"
     ]
    }
   ],
   "source": [
    "do(puzzle27)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And we can do **26 balls** under the condition that either one ball is heavier or all the balls weigh the same (so, 27 possibilities):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 137,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[①②③④⑤⑥⑦⑧⑨ ⚖ ⑱⑲⑳㉑㉒㉓㉔㉕㉖] ➔ \n",
      "   >:[②③⑤⑮⑱㉑ ⚖ ①⑥⑨⑫㉔㉕] ➔ \n",
      "      >:[②⑦㉓ ⚖ ③㉔㉖] ➔ >:+② =:+⑤ <:+③ \n",
      "      =:[③⑧⑩⑬⑮⑰⑱⑳㉓㉔㉕㉖ ⚖ ①②⑤⑥⑦⑨⑪⑫⑭⑯⑲㉑] ➔ >:+⑧ =:+④ <:+⑦ \n",
      "      <:[①⑧⑩⑱⑲㉓㉖ ⚖ ③④⑦⑨⑰㉔㉕] ➔ >:+① =:+⑥ <:+⑨ \n",
      "   =:[①②③④⑤⑥⑦⑧⑨⑩⑪⑫ ⚖ ⑮⑯⑰⑱⑲⑳㉑㉒㉓㉔㉕㉖] ➔ \n",
      "      >:[④⑪ ⚖ ⑩⑮] ➔ >:+⑪ =:+⑫ <:+⑩ \n",
      "      =:[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬ ⚖ ⑭⑮⑯⑰⑱⑲⑳㉑㉒㉓㉔㉕㉖] ➔ >:+⑬ =:∅ <:+⑭ \n",
      "      <:[③⑦⑧⑭⑯⑱㉒㉓㉔㉖ ⚖ ①②④⑤⑩⑫⑬⑰⑲⑳] ➔ >:+⑯ =:+⑮ <:+⑰ \n",
      "   <:[③⑩⑮㉓㉔㉕ ⚖ ②⑦⑪⑱㉑㉒] ➔ \n",
      "      >:[⑫⑬⑮⑯㉓ ⚖ ④⑥⑲㉕㉖] ➔ >:+㉓ =:+㉔ <:+㉕ \n",
      "      =:[②⑦⑧⑨⑫⑯⑱⑲㉑㉓㉔ ⚖ ①③⑤⑥⑩⑪⑭⑮⑰㉕㉖] ➔ >:+⑲ =:+⑳ <:+㉖ \n",
      "      <:[③④⑧⑨⑪⑭⑲⑳㉒㉓㉕ ⚖ ①②⑤⑥⑩⑫⑬⑮⑰⑱㉖] ➔ >:+㉒ =:+㉑ <:+⑱\n"
     ]
    }
   ],
   "source": [
    "do(make_puzzle(26, 3, '+='))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here's another puzzle with **26 balls**, where it is stipulated that either one of the odd-numbered balls is heavier, or one of the even-numbered balls is lighter, or there is no oddball."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{0, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, -26, -24, -22, -20, -18, -16, -14, -12, -10, -8, -6, -4, -2}\n"
     ]
    }
   ],
   "source": [
    "odd_even = {(+b if b % 2 else -b) for b in range(27)}\n",
    "print(odd_even)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "①③⑫⑭⑮⑯⑰㉒㉔ ⚖ ②⑤⑧⑩⑬⑱㉑㉕㉖ ➔ \n",
      "   >:①②⑨⑩⑳㉓㉕ ⚖ ⑤⑥⑦⑧⑪⑰㉖ ➔ \n",
      "      >:②③⑧⑨⑮⑳㉕ ⚖ ⑤⑦⑪⑲㉑㉔㉖ ➔ >:-㉖ =:+① <:-⑧ \n",
      "      =:⑥⑫⑮⑱㉑㉓㉕ ⚖ ④⑤⑬⑰⑳㉔㉖ ➔ >:+⑮ =:+③ <:-⑱ \n",
      "      <:⑨⑬⑲㉑㉓ ⚖ ②⑤⑯⑰⑳ ➔ >:-② =:-⑩ <:+⑰ \n",
      "   =:⑤⑥⑦⑧⑪⑱㉑㉕ ⚖ ③⑨⑩⑫⑮⑲⑳㉔ ➔ \n",
      "      >:②⑦⑭⑳㉑ ⚖ ⑫⑮⑯⑱㉕ ➔ >:+⑦ =:+⑪ <:-⑳ \n",
      "      =:⑧⑩⑰⑱㉒ ⚖ ④⑦⑯㉓㉕ ➔ >:-④ =:∅ <:+㉓ \n",
      "      <:③⑦⑩⑭⑮⑱⑳㉑㉓㉔㉖ ⚖ ①②④⑤⑥⑪⑫⑬⑯⑰⑲ ➔ >:-⑥ =:+⑨ <:+⑲ \n",
      "   <:⑤⑥⑯⑲⑳㉕㉖ ⚖ ②④⑧⑨⑬㉑㉒ ➔ \n",
      "      >:⑦⑧⑨⑬⑭⑳㉔㉕ ⚖ ①③⑤⑫⑮⑯⑰⑱ ➔ >:+㉕ =:-㉒ <:+⑤ \n",
      "      =:①②③④⑤⑥⑦⑧⑨⑩⑪⑫ ⚖ ⑮⑯⑰⑱⑲⑳㉑㉒㉓㉔㉕㉖ ➔ >:-㉔ =:-⑭ <:-⑫ \n",
      "      <:①②③④⑤⑥⑦⑧⑨⑩⑪ ⚖ ⑯⑰⑱⑲⑳㉑㉒㉓㉔㉕㉖ ➔ >:-⑯ =:+⑬ <:+㉑\n"
     ]
    }
   ],
   "source": [
    "do(make_puzzle(26, 3, odd=odd_even))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Another variation: **27 balls**, 3 weighings, the oddball can only be heavier, but one ball, number 27, is toxic and can't be touched or placed on the balance scale. It might, however be the heavier ball, and you still need to report it as such if it is. \n",
    "\n",
    "We describe this situation by defining a puzzle with 26 (weighable) balls, but with the oddballs including the positive ball numbers from +1 to +27, inclusive. Note  the correct `=:+㉗` notation when all three weighings are equal."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "①②③④⑤⑥⑦⑧⑨ ⚖ ⑱⑲⑳㉑㉒㉓㉔㉕㉖ ➔ \n",
      "   >:①②③⑩⑱⑳㉓ ⚖ ④⑤⑥⑯⑲㉔㉕ ➔ \n",
      "      >:①⑭⑳ ⚖ ③⑦㉖ ➔ >:+① =:+② <:+③ \n",
      "      =:⑨⑮㉑ ⚖ ①⑦⑫ ➔ >:+⑨ =:+⑧ <:+⑦ \n",
      "      <:④⑪⑫⑭⑮⑱ ⚖ ⑥⑦⑲⑳㉑㉒ ➔ >:+④ =:+⑤ <:+⑥ \n",
      "   =:①②③④⑤⑥⑦⑧⑨⑩⑪⑫ ⚖ ⑮⑯⑰⑱⑲⑳㉑㉒㉓㉔㉕㉖ ➔ \n",
      "      >:①②⑤⑥⑧⑨⑩⑬⑮⑰⑲⑳ ⚖ ③④⑦⑫⑭⑱㉑㉒㉓㉔㉕㉖ ➔ >:+⑩ =:+⑪ <:+⑫ \n",
      "      =:①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬ ⚖ ⑭⑮⑯⑰⑱⑲⑳㉑㉒㉓㉔㉕㉖ ➔ >:+⑬ =:+㉗ <:+⑭ \n",
      "      <:②③⑦⑭⑯㉑㉒㉔ ⚖ ①④⑨⑩⑬⑰⑱㉖ ➔ >:+⑯ =:+⑮ <:+⑰ \n",
      "   <:②④⑤⑥⑫⑱⑳㉔ ⚖ ①⑦⑨⑭⑰⑲㉒㉓ ➔ \n",
      "      >:①④⑦⑨⑭⑯⑰㉑㉓㉔㉕ ⚖ ③⑥⑧⑩⑪⑫⑮⑲⑳㉒㉖ ➔ >:+㉔ =:+⑱ <:+⑳ \n",
      "      =:③⑤⑥⑨⑩⑭⑱⑲㉖ ⚖ ①⑦⑧⑪⑫⑬⑰㉒㉕ ➔ >:+㉖ =:+㉑ <:+㉕ \n",
      "      <:③⑤⑥⑧⑪⑯㉒㉔ ⚖ ①②⑦⑮⑳㉑㉓㉖ ➔ >:+㉒ =:+⑲ <:+㉓\n"
     ]
    }
   ],
   "source": [
    "do(make_puzzle(26, 3, odd=range(1, 28)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Puzzles with 4 Weighings\n",
    "\n",
    "We can tackle larger puzzles. With 4 weighings, we can theoretically handle up to 3<sup>4</sup> = 81 possibilities. Can we solve for **40 balls**, heavy or light? Let's check the partition sizes:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{11: ('Bad', 22, 36, 22),\n",
       " 12: ('Bad', 24, 32, 24),\n",
       " 13: ('Bad', 26, 28, 26),\n",
       " 14: ('Bad', 28, 24, 28),\n",
       " 15: ('Bad', 30, 20, 30),\n",
       " 16: ('Bad', 32, 16, 32)}"
      ]
     },
     "execution_count": 35,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "partition_sizes(make_puzzle(40, 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Every partition is bad, leaving us with a branch of size 28 or more, which can't be handled in three remaining weighings.\n",
    "\n",
    "How about **39 balls**, heavier or lighter, with the possibility that no ball is odd? "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [],
   "source": [
    "puzzle39 = make_puzzle(39, 4, '+-=')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{11: ('Bad', 22, 35, 22),\n",
       " 12: ('Bad', 24, 31, 24),\n",
       " 13: ('Good', 26, 27, 26),\n",
       " 14: ('Bad', 28, 23, 28),\n",
       " 15: ('Bad', 30, 19, 30),\n",
       " 16: ('Bad', 32, 15, 32)}"
      ]
     },
     "execution_count": 37,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "partition_sizes(puzzle39)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We see that it might be possible to solve the puzzle by starting with 13 balls on each side. Let's try:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬ ⚖ ㉗㉘㉙㉚㉛㉜㉝㉞㉟㊱㊲㊳㊴ ➔ \n",
      "   >:④⑤⑧⑨⑭⑰㉑㉕㉗㊱㊳ ⚖ ②③⑥⑦⑪⑬㉒㉚㉜㉞㉟ ➔ \n",
      "      >:①③⑦⑨⑫⑬⑭⑯㉑㉕㉘㉚㉟㊴ ⚖ ④⑥⑩⑮⑰⑲⑳㉓㉔㉙㉛㉜㉝㊳ ➔ \n",
      "         >:①②③④⑤⑥⑦⑧ ⚖ ㉜㉝㉞㉟㊱㊲㊳㊴ ➔ >:-㉜ =:+⑨ <:! \n",
      "         =:②④⑥⑧⑫⑱㉓㉔㊴ ⚖ ⑤⑪⑭⑲㉗㉘㉙㊲㊳ ➔ >:+⑧ =:-㉞ <:+⑤ \n",
      "         <:③④⑫⑬⑮⑯㉔㉕㉝㉟㊴ ⚖ ②⑥⑲⑳㉑㉒㉓㉖㉛㉞㊱ ➔ >:+④ =:-㉚ <:-㉟ \n",
      "      =:②③⑦⑩⑪⑭⑮⑰⑱㉖㉝㉞㊱㊴ ⚖ ①④⑤⑥⑬⑲⑳㉑㉒㉓㉔㉕㉙㊲ ➔ \n",
      "         >:④⑤⑳㉒㉓㉖㉚㉝㉟㊱㊴ ⚖ ②⑨⑩⑫⑭⑯⑲㉗㉜㉞㊲ ➔ >:-㊲ =:-㉙ <:+⑩ \n",
      "         =:①⑤⑥⑨⑪⑭⑳㉔㉕㊳ ⚖ ③④⑫⑮⑱㉑㉓㉘㉝㊱ ➔ >:-㉘ =:-㉛ <:+⑫ \n",
      "         <:①⑤⑦⑧⑩⑯㉑㉓㉛㊳㊴ ⚖ ②③⑥⑭⑱⑲㉒㉔㉘㉚㉞ ➔ >:+① =:-㉝ <:-㊴ \n",
      "      <:③⑦⑧⑨⑬㉓㉖㉗㉙㉚㉜㉝㉟㊱ ⚖ ①④⑤⑥⑮⑯⑰⑱⑳㉒㉕㉛㉞㊲ ➔ \n",
      "         >:⑤⑩⑫⑬⑮㉜㉞ ⚖ ⑦⑰⑱㉒㉖㉚㊳ ➔ >:+⑬ =:+③ <:+⑦ \n",
      "         =:②㊴ ⚖ ⑪⑭ ➔ >:+② =:-㊳ <:+⑪ \n",
      "         <:②③⑦⑧⑩⑪⑬⑭⑯㉑㉒㉓㉕㉖㉙㉚㉜㉞ ⚖ ①④⑤⑥⑨⑰⑱⑲⑳㉔㉘㉛㉝㉟㊱㊲㊳㊴ ➔ >:-㊱ =:-㉗ <:+⑥ \n",
      "   =:⑨⑬⑭⑯⑰⑲㉒㉕㉚㉜㊴ ⚖ ⑥⑦⑧⑪⑫⑱㉑㉔㉗㉘㊳ ➔ \n",
      "      >:③⑤⑪⑬⑭⑮⑯㉑ ⚖ ②⑨⑰⑲㉔㉖㉙㉟ ➔ \n",
      "         >:②④⑧⑯㉖㉘ ⚖ ①⑥⑨⑩⑭㉟ ➔ >:+⑯ =:-㉔ <:+⑭ \n",
      "         =:⑤⑥⑦⑩⑭⑯㉒㉖㉗㉛㉝㊲㊳ ⚖ ②③⑧⑪⑫⑬⑮⑰⑳㉑㉔㉕㉟ ➔ >:+㉒ =:-⑱ <:+㉕ \n",
      "         <:⑧⑯㉔㉗㉟ ⚖ ⑬⑭⑲㉑㊲ ➔ >:-㉑ =:+⑰ <:+⑲ \n",
      "      =:①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰ ⚖ ㉓㉔㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟㊱㊲㊳㊴ ➔ \n",
      "         >:①④⑥⑨⑪⑫⑯⑲⑳ ⚖ ③⑦⑮㉖㉙㉞㉟㊱㊲ ➔ >:-㉖ =:-㉓ <:+⑮ \n",
      "         =:③④⑩⑪⑬⑮⑯⑰⑱⑲⑳㉑㉒㉓㉗㉚㉟㊱㊳ ⚖ ①②⑤⑥⑦⑧⑨⑫⑭㉔㉕㉖㉘㉛㉜㉝㉞㊲㊴ ➔ >:+⑳ =:∅ <:-⑳ \n",
      "         <:②③④⑥⑦⑬⑯⑰⑱⑲⑳㉒㉖㉚㉜㉝㉟㊴ ⚖ ①⑤⑧⑩⑪⑫⑭㉑㉓㉔㉕㉗㉘㉙㉛㉞㊱㊲ ➔ >:+㉖ =:-⑮ <:+㉓ \n",
      "      <:①③④⑬⑰⑱⑲㉑㉒㉔㉗㉞㊳㊴ ⚖ ②⑤⑦⑧⑫⑮⑳㉓㉖㉘㉙㉛㊱㊲ ➔ \n",
      "         >:①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱ ⚖ ㉒㉓㉔㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟㊱㊲㊳㊴ ➔ >:+⑱ =:+㉑ <:+㉔ \n",
      "         =:①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮ ⚖ ㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟㊱㊲㊳㊴ ➔ >:-㉕ =:-⑯ <:-⑭ \n",
      "         <:①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱ ⚖ ㉒㉓㉔㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟㊱㊲㊳㊴ ➔ >:-㉒ =:-⑲ <:-⑰ \n",
      "   <:②⑩⑪⑮⑳㉓㉖㉗㉙㉞㉟ ⚖ ①④⑦⑧⑫⑰㉜㉝㊲㊳㊴ ➔ \n",
      "      >:②⑤⑥⑩⑪⑬㉑㉓㉔㉕㉖㉗㉚㉜㉝㊴ ⚖ ①③⑦⑭⑮⑯⑰⑱⑲㉒㉙㉛㉞㉟㊲㊳ ➔ \n",
      "         >:②③④⑤⑥⑨⑩⑫⑭⑮㉒㉓㉖㉘㉜㉞㉟㊱㊲ ⚖ ①⑧⑪⑬⑯⑰⑱⑲⑳㉑㉔㉕㉗㉙㉚㉛㉝㊳㊴ ➔ >:-① =:-⑦ <:+㉗ \n",
      "         =:⑫⑭⑮⑯㉑㉔㉗㉘㉚㉜㊴ ⚖ ①②③⑧⑨⑩⑲⑳㉖㉞㊳ ➔ >:-⑧ =:-④ <:-⑫ \n",
      "         <:①⑦⑩⑬⑭⑯⑲⑳㉕㉝㉟㊱㊴ ⚖ ②④⑤⑧⑪⑫⑱㉒㉔㉖㉗㉙㊲ ➔ >:+㉟ =:+㉞ <:+㉙ \n",
      "      =:⑬⑯㉒㉗㉘㉚㊴ ⚖ ⑥⑦㉓㉖㉛㊱㊲ ➔ \n",
      "         >:⑥⑬⑰⑱⑳㉒㉕㉗㉘㉙㉛㉜㉞㊱㊲ ⚖ ①②④⑤⑦⑧⑨⑩⑫⑮⑲㉓㉔㉟㊴ ➔ >:+㉘ =:+㉚ <:-⑥ \n",
      "         =:①④⑤⑥⑫⑮⑲㉒㉔㉚㉝㉞ ⚖ ⑧⑨⑪⑬⑯㉕㉖㉗㉘㊱㊳㊴ ➔ >:-⑨ =:-③ <:-⑤ \n",
      "         <:②④⑥⑪㉙㉚㊱ ⚖ ⑳㉓㉘㉛㉞㉟㊲ ➔ >:+㊱ =:-⑬ <:+㉛ \n",
      "      <:③⑤⑨⑬⑭⑮⑯⑱㉑㉕㉚㊱㊲㊳ ⚖ ①④⑦⑩⑫⑰⑲㉒㉖㉘㉙㉜㉞㊴ ➔ \n",
      "         >:②⑥⑩⑯⑰⑳㉒㉓㉖㉘㉙㉚㉝㉟㊲ ⚖ ③⑤⑧⑨⑫⑬⑭⑮⑱㉕㉗㉛㉞㊱㊴ ➔ >:+㊲ =:+㊳ <:-⑩ \n",
      "         =:②⑱㉕㉙㉝㊴ ⚖ ⑦⑧⑩⑳㉗㊱ ➔ >:+㉝ =:-⑪ <:-② \n",
      "         <:① ⚖ ㊴ ➔ >:! =:+㉜ <:+㊴\n"
     ]
    }
   ],
   "source": [
    "do(puzzle39)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "How about **80 balls, 4 weighings** under the condition that no ball can be heavier (thus, 81 possibilities, the maximum)?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 138,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳㉑㉒㉓㉔㉕㉖㉗ ⚖ ⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓ] ➔ \n",
      "   >:[①③⑦⑬㉒㉔㉗㉜㊱㊵㊷㊿ⒽⓁⓅⓇⓈⓉⓍⓑⓒ ⚖ ⑥⑨⑩⑲⑳㉕㉙㉚㊳㊶㊾ⒷⒹⒻⒼⒾⓂⓃⓆⓊⓐ] ➔ \n",
      "      >:[⑥⑧⑬㉝㊲㊿ⒶⒻⓁⓂⓃⓌⓏ ⚖ ③㉒㉔㉖㉘㊽ⒹⒼⒽⒿⓐⓒⓓ] ➔ \n",
      "         >:[⑤⑫⑲㉓㉛ⒷⒸⒽⒾⓐⓒ ⚖ ①②③④⑬㉕㊷ⒹⓂⓄⓋ] ➔ >:-Ⓓ =:-Ⓖ <:-ⓐ \n",
      "         =:[④⑥⑪⑯⑲㉓㉕㉗㉘㉙㉝㉟㊱㊵㊹㊺㊻㊿ⒶⒺⒻⒼⒽⓂⓃⓅⓆⓈⓌⓒ ⚖ ①⑤⑦⑬⑭⑮⑱㉚㉜㉞㊲㊴㊷㊸㊼㊽㊾ⒷⒸⒹⒾⒿⓀⓁⓋⓍⓎⓏⓑⓓ] ➔ >:-Ⓘ =:-Ⓤ <:-Ⓠ \n",
      "         <:[⑧⑨⑩㉚㊲ⓂⓅⓈ ⚖ ⑫㊱㊹ⒷⒸⒻⒼⒿ] ➔ >:-Ⓕ =:-Ⓝ <:-Ⓜ \n",
      "      =:[②⑲㉕㉘㉙㉟㊱㊴㊵㊽㊿ⒼⓀⓄⓆⓉⓎⓑ ⚖ ⑥⑧⑨⑫⑰⑱㉓㉔㉗㉞㊸ⒸⒹⒿⓂⓈⓌⓏ] ➔ \n",
      "         >:[①⑥⑦⑫⑱㉒㉔㉖㉚㊳㊷㊸㊻㊽ⒶⒷⒸⒼⓁⓂⓆⓇⓏⓓ ⚖ ④⑤⑧⑩⑬⑲㉗㉛㉝㉞㉟㊱㊴㊶㊹㊺㊾㊿ⒽⒾⓃⓌⓎⓑ] ➔ >:-Ⓦ =:-Ⓙ <:-Ⓩ \n",
      "         =:[③④⑥⑬⑰⑱⑲㉜㉝㉞㉟㊺㊻ⒼⒾⒿⓁⓆⓇⓏⓐⓓ ⚖ ①②⑤⑦⑧⑨⑭⑳㉓㉕㉗㊲㊳㊸㊾ⒶⒷⒸⓀⓂⓊⓋ] ➔ >:-Ⓥ =:-Ⓔ <:-ⓓ \n",
      "         <:[①④⑦⑩⑫⑬㉒㉖㉗㉘㉟㊱㊳㊴㊶㊹㊿ⒶⒷⒹⒼⒾⒿⓅⓇⓋⓌⓎⓐⓑ ⚖ ⑥⑧⑨⑪⑮⑯⑰⑱⑲㉑㉓㉔㉕㉚㉛㊵㊸㊻㊼㊽㊾ⒺⒽⓁⓃⓄⓆⓈⓉⓏ] ➔ >:-Ⓞ =:-Ⓚ <:-Ⓨ \n",
      "      <:[④⑦⑩⑪⑬⑱㉓㉔㉖㉗㉚㉜㊳㊴㊺ⒶⒸⒹⒾⓀⓃⓆⓇⓉⓌⓏⓑ ⚖ ①③⑥⑧⑨⑭⑲㉒㉛㉝㊵㊷㊸㊻㊽㊾㊿ⒺⒿⓁⓂⓄⓅⓈⓋⓎⓐ] ➔ \n",
      "         >:[④⑲㉕㉚㊳㊹ⒷⒾⓅⓌⓑⓒ ⚖ ②㉑㉓㉔㉝㉟㊺ⓁⓄⓆⓏⓓ] ➔ >:-Ⓛ =:-Ⓢ <:-Ⓟ \n",
      "         =:[②⑨⑬⑭㉚㊴㊵㊸㊺㊼㊾ⒷⒸⒹⒼⒾⒿⓃⓏⓒ ⚖ ④⑧⑩⑯⑱㉓㉔㉖㉘㉛㉜㉟㊳㊷ⒶⓊⓌⓍⓎⓐ] ➔ >:-Ⓧ =:-Ⓗ <:-ⓒ \n",
      "         <:[③㊲ⒹⓂⓐⓑ ⚖ ㊷ⒺⓀⓄⓇⓍ] ➔ >:-Ⓡ =:-Ⓣ <:-ⓑ \n",
      "   =:[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳㉑㉒㉓㉔㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟㊱ ⚖ ㊺㊻㊼㊽㊾㊿ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓ] ➔ \n",
      "      >:[①④⑤⑦⑨⑯⑰⑲㉖㉞㊴㊶㊹㊻㊼ⒷⒹⓃⓅⓈⓉⓎⓏⓐⓒ ⚖ ②③⑮⑱⑳㉑㉒㉓㉔㉕㉛㉝㉟㊱㊾ⒶⒸⒺⒻⒽⒾⓂⓄⓆⓌ] ➔ \n",
      "         >:[①④⑭⑮⑲㉘㉟㊳㊵㊸㊽ⒸⒺⒾⒿⓊⓌⓏ ⚖ ③⑥⑦⑪㉓㉖㉗㉜㊹㊻㊼㊾㊿ⒷⒹⒼⓈⓎ] ➔ >:-㊾ =:-Ⓐ <:-Ⓒ \n",
      "         =:[㉔㉖㊷㊿ⒷⓋⓌ ⚖ ⑪㉓㊻㊼㊽ⒽⓂ] ➔ >:-㊽ =:-㊺ <:-㊿ \n",
      "         <:[②⑥⑭㉟㊴㊻㊽ⒸⓃⓅⓐⓓ ⚖ ③⑤⑧⑪⑰㉗㊵㊶ⒷⒺⓆⓎ] ➔ >:-Ⓑ =:-㊼ <:-㊻ \n",
      "      =:[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳㉑㉒㉓㉔㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟㊱㊲㊳㊴ ⚖ ㊷㊸㊹㊺㊻㊼㊽㊾㊿ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓ] ➔ \n",
      "         >:[⑧⑨⑩⑬⑰⑳㉔㉕㉜㉞㉟㊳㊶㊹㊽㊿ⒷⒸⓃⓅⓊⓍⓐ ⚖ ⑥⑦⑭㉖㉘㉙㉝㊴㊵㊸㊼㊾ⒶⒺⒼⒿⓁⓄⓆⓇⓉⓏⓓ] ➔ >:-㊸ =:-㊷ <:-㊹ \n",
      "         =:[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳㉑㉒㉓㉔㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟㊱㊲㊳㊴㊵ ⚖ ㊶㊷㊸㊹㊺㊻㊼㊽㊾㊿ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓ] ➔ >:-㊶ =:∅ <:-㊵ \n",
      "         <:[③④⑥⑦⑪⑳㉒㉕㉖㉗㉚㉜㊱㊲㊶㊷㊸㊹㊺㊾㊿ⒸⒼⒾⓁⓃⓅⓈⓉⓊⓍⓒ ⚖ ①②⑧⑩⑫⑭⑮⑯⑱㉓㉘㉙㉛㉞㉟㊴㊵㊻㊽ⒶⒹⒺⒽⓀⓂⓄⓆⓇⓎⓏⓐⓓ] ➔ >:-㊴ =:-㊳ <:-㊲ \n",
      "      <:[①③⑤⑥⑧⑨⑩⑫⑰⑲⑳㉛㉜㊱㊳㊴㊵㊶㊷㊾ⓁⓃⓅⓋⓏⓑⓓ ⚖ ④⑪⑮㉖㉗㉘㉙㉚㊸㊹㊺㊻ⒶⒸⒺⒻⒾⒿⓀⓄⓇⓈⓉⓌⓎⓐⓒ] ➔ \n",
      "         >:[①④⑦⑨⑬⑭⑮⑳㉒㉔㉗㉘㊲㊳㊵㊶㊿ⒶⒸⒻⒿⓈⓊⓏⓒ ⚖ ②⑤⑩⑫⑱⑲㉖㉙㉜㉝㉟㊷㊸㊽㊾ⒷⒺⒼⒾⓂⓄⓆⓇⓍⓎ] ➔ >:-㉙ =:-㉚ <:-㉘ \n",
      "         =:[⑩⑱㉒㉕㉞ⒺⓍⓑⓒ ⚖ ⑦⑨⑪⑬㉖㉚㉝㊳㊾] ➔ >:-㉝ =:-㉟ <:-㉞ \n",
      "         <:[⑨⑩⑪⑬㉖㉘㉜㉝㊲㊴㊷㊸㊹㊻㊿ⒶⒸⓂⓆⓈⓋⓎⓐⓑⓓ ⚖ ②④⑦⑯⑲⑳㉓㉔㉗㉙㉚㉛㊽㊾ⒷⒾⒿⓀⓅⓇⓉⓊⓌⓏⓒ] ➔ >:-㉛ =:-㊱ <:-㉜ \n",
      "   <:[⑥⑦⑨⑫⑯⑱㉒㉓㉖㉙㉜㉝㉞㊱㊵㊷㊼ⒶⒸⒺⒻⒼⓁⓃⓋⓐⓑⓒ ⚖ ②③⑩⑪⑲⑳㉔㉕㉗㉘㉚㉟㊸㊹㊺㊻㊾ⒽⒿⓀⓂⓄⓅⓇⓈⓉⓌⓎ] ➔ \n",
      "      >:[⑧⑨⑪⑮⑯⑱⑲㉑㉒㉔㉚㉛㉟㊱㊴㊶㊷㊿ⒶⒸⒹⒺⒼⓀⓁⓈⓑ ⚖ ①②③⑰⑳㉓㉘㉞㊲㊵㊺㊼㊽ⒷⒽⒿⓄⓅⓆⓉⓋⓌⓍⓎⓏⓒⓓ] ➔ \n",
      "         >:[②⑤㉒㉕㉘㉙㉚㉛㉟㊱㊶ⒹⒽⓋⓑ ⚖ ③⑦⑩⑪⑯⑰㉔㉝㊴㊸㊹ⒶⒿⓆⓓ] ➔ >:-③ =:-⑳ <:-② \n",
      "         =:[③⑥⑩⑫⑭⑳㉝㉟㊿ⒽⒿⓁⓄⓅⓈ ⚖ ④⑦⑪⑱㉑㉔㉕㉛㉜㊴㊼ⒾⓉⓎⓓ] ➔ >:-㉕ =:-㉗ <:-⑩ \n",
      "         <:[①③④⑨⑫⑲㉑㉓㉗㊳㊶㊷㊺㊼㊾㊿ⒸⒹⒺⒽⒿⓀⓄⓈⓉⓎⓏⓑ ⚖ ②⑥⑩⑪⑮⑯⑰⑱⑳㉕㉖㉛㉜㉞㊴㊻㊽ⒻⒾⓁⓂⓆⓇⓊⓌⓍⓐⓓ] ➔ >:-⑪ =:-㉔ <:-⑲ \n",
      "      =:[④⑤⑥⑦⑨⑩⑬⑲㉒㉓㉔㉗㉘㉝㉟㊱㊷㊹㊼㊽㊾㊿ⒹⒻⒽⒿⓀⓁⓂⓋⓎⓒ ⚖ ①②③⑧⑪⑫⑯⑰⑱⑳㉖㉙㉚㉛㊲㊳㊴㊵㊶㊻ⒶⒷⒸⒺⓃⓄⓇⓉⓌⓏⓐⓑ] ➔ \n",
      "         >:[④⑤⑩⑫⑭⑮⑰⑲㉑㉒㉛㉝㉟㊲㊳㊴㊻㊽㊾ⒷⒹⒻⒽⒿⓋⓍⓓ ⚖ ③⑥⑧⑪⑱㉓㉔㉕㉖㉚㉜㉞㊱㊵㊷㊹㊺㊿ⒶⒸⓁⓅⓊⓌⓎⓏⓑ] ➔ >:-⑧ =:-① <:-⑰ \n",
      "         =:[⑨⑭⑳㉕㉖㉗㉛㉞㊸ⒶⒻⓀⓅⓇ ⚖ ⑥⑫⑬⑮㉚㊹㊺㊿ⒹⒺⒿⓊⓌⓎ] ➔ >:-⑮ =:-㉑ <:-⑭ \n",
      "         <:[③⑤⑥⑧⑨⑰⑳㉑㉒㉓㉔㉕㉗㉘㉙㉚㉝㊲㊵㊶㊸ⒷⒺⒻⓀⓁⓂⓃⓄⓊⓐⓑⓒⓓ ⚖ ①②⑩⑪⑫⑬⑭⑮⑯⑱㉖㉜㉞㉟㊱㊳㊴㊹㊺㊻㊽㊿ⒶⒸⒹⒽⒿⓆⓇⓈⓉⓋⓌⓎ] ➔ >:-⑬ =:-④ <:-⑤ \n",
      "      <:[②⑯㉓㉕㉖㉘㉛㊲㊵㊶㊸ⒸⒺⒼⒿⓂⓅⓊⓎ ⚖ ④⑤⑥⑦⑧⑩⑬㉒㉙㉚㉜㉝㊼ⒶⒻⓆⓉⓑⓒ] ➔ \n",
      "         >:[②⑥⑬⑯⑱㉔㉖㉙㉜㉟㊱㊷㊽ⒷⒹⒻⒾⓆⓈⓉⓐⓒⓓ ⚖ ①③⑤⑪⑰㉑㉒㉚㉞㊶㊺㊼㊿ⒶⒸⒺⒼⓂⓅⓇⓊⓋⓎ] ➔ >:-㉒ =:-⑦ <:-⑥ \n",
      "         =:[①⑥⑦⑧⑫⑭⑯⑲㉔㉖㉘㉚㉞㉟㊲ⒼⓄⓆⓇⓋⓍⓐⓓ ⚖ ③⑬⑮⑰⑱⑳㉑㉓㊴㊹㊾㊿ⒹⒺⒻⒾⓁⓅⓉⓊⓌⓏⓑ] ➔ >:-⑱ =:-⑨ <:-⑫ \n",
      "         <:[③⑨⑪㉖㉜㉟㊲㊵㊶㊷㊼ⒻⒼⓁⓄⓅⓆⓇⓈⓏⓒⓓ ⚖ ④⑧⑩⑭⑲㉓㉔㉗㉚㊴㊹㊽㊾㊿ⒶⒷⒹⓂⓃⓊⓍⓐ] ➔ >:-㉓ =:-⑯ <:-㉖\n"
     ]
    }
   ],
   "source": [
    "do(make_puzzle(80, 4, '-='))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Looking good (except that the output is a bit hard to read and understand, because it is so large).\n",
    "\n",
    "# Puzzles with 5 or More Weighings\n",
    "\n",
    "With 5 weighings I could conceivably handle up to 3<sup>5</sup> = 243 oddballs, and with 6 weighings, 3<sup>6</sup> = 729. The resulting policy trees would be multiple pages of output, so I don't think I need to see them. Instead, I'll make a **table** of results. For various values of *w* from 3 up, and for the oddballs being each of '+-', '+-=', '+', '+=', I want to know what's the highest number of balls, *N*, that leads to a solvable puzzle (and the total number of oddballs, which will be either *N*, 2*N*, *N* + 1, or 2*N* + 1)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 139,
   "metadata": {},
   "outputs": [],
   "source": [
    "def report(w_range=range(3, 7), odds=('+-', '+-=', '+', '+=')):\n",
    "    headers = ['*w*','3<sup>*w*</sup>', *(f'odd: \"{odd}\"' for odd in odds)]\n",
    "    return format_table(headers, [[w, 3 ** w, *(highest_N(w, odd) for odd in odds)] \n",
    "                                  for w in w_range])\n",
    "    \n",
    "def highest_N(w, odd: str) -> Optional[Tuple[int, int]]:\n",
    "    \"\"\"Find highest number of balls N (and total number of oddballs) that are solvable in w weighings.\"\"\"\n",
    "    target = 3 ** w // (odd.count('+') + odd.count('-'))\n",
    "    for N in reversed(range(target + 3)):\n",
    "        puzzle = make_puzzle(N, w, odd)\n",
    "        if any(good_partitions(puzzle, repeat=1)) and solve(puzzle):\n",
    "            return (N, len(puzzle.oddballs))\n",
    "    return None # No solution"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "I decided to format the table with IPython Markdown:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 140,
   "metadata": {},
   "outputs": [],
   "source": [
    "from IPython.display import Markdown\n",
    "\n",
    "def format_table(headers, rows, justify='---:') -> Markdown:\n",
    "    \"\"\"Make a markdown table from header and rows.\"\"\"\n",
    "    underline = [justify for _ in headers]\n",
    "    lines = [headers, underline, *rows]\n",
    "    return Markdown(cat(map(format_row, lines)))\n",
    "\n",
    "def format_row(columns) -> str: \n",
    "    \"\"\"Format a single row of a table in Markdown format.\"\"\"\n",
    "    columns = map(str, columns)\n",
    "    return \"|\" + \"|\".join(columns) + \"|\\n\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CPU times: user 5.39 s, sys: 17.4 ms, total: 5.41 s\n",
      "Wall time: 5.41 s\n"
     ]
    },
    {
     "data": {
      "text/markdown": [
       "|*w*|3<sup>*w*</sup>|odd: \"+-\"|odd: \"+-=\"|odd: \"+\"|odd: \"+=\"|\n",
       "|---:|---:|---:|---:|---:|---:|\n",
       "|1|3|None|None|(3, 3)|(2, 3)|\n",
       "|2|9|(3, 6)|(3, 7)|(9, 9)|(8, 9)|\n",
       "|3|27|(12, 24)|(12, 25)|(27, 27)|(26, 27)|\n",
       "|4|81|(39, 78)|(39, 79)|(81, 81)|(80, 81)|\n",
       "|5|243|(120, 240)|(120, 241)|(243, 243)|(242, 243)|\n"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "execution_count": 58,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "%time report(range(1, 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# What's Next?\n",
    "\n",
    "- What other puzzles can you solve?\n",
    "- Can you *prove* the unsolved puzzles are unsolvable? \n",
    "- Can you find trees that minimize the *mean* number of weighings, rather than minimizing the *maximum* number of weighings?\n",
    "- Can you minimize the number of balls in each weighing? The solutions above sometimes weigh 3 or 4 balls on each side of the scale when only 1 or 2 were necessary.\n",
    "- Currently, when `solve` returns `None`, it means \"no solution was found, but there's no proof that a solution doesn't exist.\" Can you modify `solve` to return a result that indicates there is no possible solution? (Maybe you can only do this in a reasonable amount of time on smaller puzzles, not all.)\n",
    "- What about puzzles where your task is to identify *which* ball is odd, but not *how* it is odd? That is, if you get the possible oddballs down to `{-3, +3}` then you're done; you know ball 3 is the odd one, and you don't care if it is heavy or light.\n",
    "- More generally, can you solve puzzles when it is a possibility (or a requirement) that *two* balls are odd?  Or more than two?\n",
    "- What if you had two or more balance scales that you could use in parallel, and the goal was to minimize the number of weighing time periods, not the total number of weighings?\n",
    "- What else can you discover?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Better Partitions\n",
    "\n",
    "I defined `good_partitions` to randomly guess at a good partition; that worked fine for small puzzles. But for larger puzzles, I want to do a more systematic job. Here's what I'm thinking:\n",
    "\n",
    "- I should be focusing on **oddballs**, not balls.\n",
    "- Balls with the same information should be treated the same. For example, at the start of a puzzle where all balls might be lighter or heavier, it matters how many balls I put on each side of the scale, but it doesn't matter which balls; `①②③④` is equivalent to `③⑥⑧⑪`.\n",
    "- Balls with different information should be treated differently. If the set of oddballs has the subset {+1, -1, +2} then I get more information from putting ball 1 on the scale (it might turn out to be heavy or light) than ball 2 (which we already know can not be light)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 152,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'+-': {1, 2}, '+': {3, 5}, '-': {4, 6}, '': {7, 8}}"
      ]
     },
     "execution_count": 152,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def divide(puzzle) -> Dict[str, Set[Ball]]:\n",
    "    oddballs, balls = puzzle.oddballs, puzzle.balls\n",
    "    return {'+-': {b for b in balls if b in oddballs and -b in oddballs},\n",
    "            '+':  {b for b in balls if b in oddballs and -b not in oddballs},\n",
    "            '-':  {b for b in balls if -b in oddballs and b not in oddballs},\n",
    "            '':   {b for b in balls if b not in oddballs and -b not in oddballs}}\n",
    "\n",
    "puz = Puzzle(8, 3, balls=range(1, 9), oddballs={0, 1, -1, 2, -2, 3, -4, 5, -6})\n",
    "divide(puz)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[[1, 2, 3]]"
      ]
     },
     "execution_count": 43,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from collections import defaultdict\n",
    "\n",
    "def divide(oddballs):\n",
    "    dic = defaultdict(list)\n",
    "    for ball in set(map(abs, oddballs)) - {0}:\n",
    "        dic[ball in oddballs, -ball in oddballs].append(ball)\n",
    "    return [(sum(x), dic[x]) for x in dic]\n",
    "\n",
    "def pick_L(division, points_needed: int) -> Iterator[List[Ball]]:\n",
    "    #print('call pick_L', division, points_needed)\n",
    "    if points_needed == 0:\n",
    "        yield []\n",
    "    elif points_needed > 0 and division:\n",
    "        [(points, balls), *rest] = division\n",
    "        for p in range(0, points_needed // points + 1):\n",
    "            #print('  working on', balls[:p])\n",
    "            for remainder in pick_L(rest, points_needed - p * points):\n",
    "                yield balls[:p] + remainder\n",
    "\n",
    "def good_partitionsXXX(puzzle) -> Iterable[Weighing]:\n",
    "    \"Yield good partitions.\"\n",
    "    oddballs, weighings, balls = puzzle.oddballs, puzzle.weighings, list(puzzle.balls)\n",
    "    if isinstance(puzzle.odd, str):\n",
    "        tries = 1 # First weighing, undifferentiated balls: only need to try once.\n",
    "    for _ in range(tries): \n",
    "        for B in range(1, 1 + len(balls) // 2):\n",
    "            L, R = balls[:B], balls[-B:]\n",
    "            partition = weigh(L, R, oddballs)\n",
    "            if is_good_partition(partition, weighings - 1):\n",
    "                yield partition\n",
    "        random.shuffle(balls)\n",
    "\n",
    "list(pick_L(divide(puzzle12.oddballs), 6))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "metadata": {},
   "outputs": [],
   "source": [
    "# sorted(range(1, N // 2 + 1), key=distance_to(N / 3)):\n",
    "def distance_to(target): return lambda x: abs(x - target) \n",
    "\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
